iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Modern Web

再造輪子:30 天臨摹 React Hook 函式庫,探索背後的設計哲學系列 第 29 篇

【 Day 28 】同一個問法,換了複雜度,答案會換形狀嗎?

  • 分享至 

  • xImage
  •  

昨天我們把六個領域的問題句並排,從裡面找出了四條軸。訂閱在樞紐之前,就建立了三種問法;失效則走過樞紐,到了真實 DOM,還在增加需要處理的情境。

這兩條軸,都是一個領域接著一個領域累積過來的。

抽象不太一樣。昨天最後回到 Day 19 的那句話:真正的抽象單位,是「一個 Storage 的一個 key」。然後留下了一個問題:為什麼認出這個單位,會改變模組能替呼叫端承擔的事情?

瀏覽器 API 那四天,已經給過一個答案。只是那一次,我們看的,是全季介面最簡單的一個 API。

同一個問法,換到更複雜的地方,還能看出差別嗎?看出來的差別,又會落在哪裡?

讓我們先回到樞紐之前,看看抽象在那三個領域留下了什麼,再把瀏覽器 API 找到的問法,帶到樞紐之後的兩個領域。

樞紐之前,三次沒有比完的比較

訂閱在前三個領域裡,每經過一個領域,就多問了一個面向。抽象在同樣的三個領域裡,留下的卻是三次沒有比完的比較。

遠端世界的最後一天,TanStack Query 與 SWR 的介面寬度相近,深度不同。深的那一側承接得多,而它承接的東西,會以概念的形式回到呼叫端身上。至於什麼時候值得付那個代價,Day 07 寫的是:我現在還沒有可以拿來量的東西。

共享狀態的最後一天,也就是 Day 11 時,Zustand、Jotai、Valtio 在我們的比較裡,介面越來越窄,深度越來越深。可是承接得越多,我越難從程式碼看出它替我做了什麼決定,當時,我們把這個感覺先放著。

在 Day 15,使用者輸入的最後一天,RHF 與 TanStack Form 兩個都深,深在不同地方。前兩次至少還說得出哪一份承接得比較多;這次,連這一點都說不出來。

在探索瀏覽器 API 的最後一天,Day 19 把這三組放上同一張圖,它們是三種不同的形狀:一段垂直的落差、一條往上斜的軌跡,以及一條橫線。

後一個形狀,並沒有回答前一個留下的問題。Day 11 的軌跡,沒有告訴我們 Day 07 的落差何時值得;Day 15 的橫線,則讓「哪一份承擔得比較多」也不再夠用。

它們比較像是三個樣本,差別看見了,卻還說不清楚。

前三次,我主要在問:模組替呼叫端承擔了多少?兩份答案承擔得差不多時,只問份量,就很難再往下比。

瀏覽器 API:從另一端開始問

到了瀏覽器 API,我們開始從另一端問。

Day 17 問:把 localStorage 包成 Hook 之後,呼叫端還需要知道什麼?

這個問法,讓我們把視線從模組承擔的份量,移到承擔之後,還有哪些事情留給呼叫端。寬度與深度仍然在,只是現在可以沿著具體的使用情境,看它們落在哪裡。

拿這個問句回頭看 Day 15,至少有一個差別開始說得清楚。在我們比較的訂閱寫法裡,TanStack Form 要呼叫端明確交出選擇器,需要控制比較方式時,再交一個比較函式。要知道怎麼選、怎麼比,就是這個介面留給呼叫端的事情。

這還不足以替兩份表單函式庫排出高下,但比「兩個都很深」多說出了一點東西。

回到瀏覽器 API 自己的三份答案,差別則落在:Hook 用什麼單位,把外面的值與 React 的訂閱組織起來?

react-use 把值讀進來之後,放進這個元件自己的狀態。同一個 key 被另一個元件使用時,兩邊怎麼保持同步,留給呼叫端處理。

usehooks-ts 透過 window 上的通知,把變動傳給其他使用者;收到通知的元件,再各自回頭重讀。

@react-hookz/web 則以「一個 Storage 的一個 key」為單位,組織值與訂閱者。寫入、刪除與另一個分頁送來的事件,走的都是同一份登記簿;寫入的元件自己,也只是同一個 key 的訂閱者之一。

昨天那個問題,答案大概就在這裡。

當「一個 Storage 的一個 key」成為模組內部的單位,讀取、寫入與訂閱就有了共同的歸屬。同一個 key 的其他觀察者,也能放進同一套機制裡處理。

認出這個單位,讓需要一起處理的事情,有了可以一起放的地方。呼叫端原本要記得的關係,也就有機會由模組承擔。

不過,這個答案是在一個介面很簡單的 API 上得到的。localStorage 的讀寫方法不多,我們追的複雜度,主要在它跟 React 元件模型接起來的地方。

Day 19 收尾時說過,這個問法等到了真實 DOM,會在最難的那一端再用一次。在那之前,中間還隔著互動行為。

互動行為:份量相近,接法不同

互動行為,是這個問法離開瀏覽器 API 之後,第一個走到的地方。

那三天裡,我們把同一份焦點合約,交成了三種形狀:一包 prop getter、一疊行為層,以及一棵元件樹加上一個打洞的 API。三份臨摹甚至從同一個檔案匯入合約;Day 22 推測,三個原作也都認出了焦點合約,才是要交出去的單位。

只看承擔的份量,三者大概會疊在同一個位置上。這很像 Day 15 卡住的情況。

但這一次,我們可以繼續問:呼叫端還需要知道什麼?

Day 22 得到的答案,份量相近,形狀卻不同:一個要知道貼在哪,一個要知道切在哪一層,一個要知道怎麼打洞。

三份都承擔了焦點合約,但把合約接到呼叫端 DOM 上的方法不同。這些接法,就是呼叫端仍然需要理解的事情。

差別於是落在介面寬度。寬度在這裡看的,是呼叫端以多大的單位接到行為:逐個元素接、一層一層接,或先接上一棵元件樹,再替換其中的元素。

同一個問句,到了互動行為,仍然看得出差別。只是這次看見的,是合約怎麼交到呼叫端手上。

真實 DOM:承擔之後,判斷留在哪裡?

到了真實 DOM,兩份答案要承擔的事情,又更複雜了一些。

清單裡的一列,要先畫出來才知道多高;量到之後,又可能改變位置,以及下一次要畫哪幾列。輸出回到了輸入,Day 26 讓這個迴圈在模擬的瀏覽器裡轉了起來。

在我們的臨摹與測試情境裡,TanStack Virtual 與 react-virtuoso 都承擔了量測與位置之間的回饋,也都讓這個迴圈停了下來。

只看這一點,仍然很難說哪一份承擔得比較多。

差別落在另一個地方:預設的補償判斷不合用時,呼叫端有沒有地方接手?

TanStack Virtual 把該不該補償的判斷做成公開的屬性,附上預設規則。呼叫端可以沿用,也可以替換。

沿用時,這個判斷可以留給模組;需要替換時,呼叫端就得理解,什麼樣的尺寸變化應該補償。這個入口交出了決定權,也把做決定所需的知識交到了呼叫端手上。

react-virtuoso 把我們追的那條補償規則留在內部。呼叫端不必先替它決定,但也沒有同樣的入口,可以直接替換那個判斷。

所以,「呼叫端還需要知道什麼?」在這裡要連著使用情境一起問:預設符合需求時,需要知道多少;預設不合用時,又得知道多少,才能把判斷接過來?

Day 26 說「深的形狀不同」,差別就在這裡。兩份都把回饋迴圈收進了內部,卻替其中的一個判斷,留下了不同的邊界。

同一個問法,答案換了形狀

現在,讓我們把這幾次得到的答案放在一起。

  • 瀏覽器 API:值與訂閱以什麼單位組織,哪些同步關係仍要由呼叫端處理?
  • 互動行為:焦點合約交成什麼單位,呼叫端要知道怎麼接到 DOM 上?
  • 真實 DOM:預設判斷不合用時,呼叫端能不能接手,又需要理解什麼?

問的都是同一句:呼叫端還需要知道什麼?

答案卻從同步關係,走到合約的接法,再走到判斷的接手。

這幾次之間,問法沒有變,變的是被包起來的東西有多複雜。複雜度在這裡扮演了什麼角色?

先看瀏覽器 API。沒有把同一個 key 的訂閱者組織在一起的 react-use,仍然可以用。Day 19 說過,對一個只有一個元件在讀的值,這樣就夠了;需要多個元件同步時,那個需求才會把留在呼叫端的事情照出來。

再看真實 DOM。Day 26 的五個動態高度現場裡,只靠固定高度推算已經不夠。Day 23 的清單停得最快,畫出來的列卻壓到了下一列。

兩份會量測的實作,都承擔了量測與位置之間的回饋。於是我們接著看的,是承擔之後,還有哪些判斷留在邊界上。

這讓我開始懷疑:當一件事成為處理情境的必要條件,差別就可能往後移,移到模組留下哪些判斷、又讓呼叫端如何接手。

這是從幾份臨摹得到的推測。兩端面對的需求不同,拿來比較的差異也不同,現在還不能把答案換形狀,全都歸因於複雜度。

但至少有一件事已經看見了:同一個問法,能帶著我們走到不同的領域。它讓我們繼續往下問,直到找到呼叫端仍然需要理解或決定的地方。

沿著抽象這條線,看見兩幕

回頭看,樞紐之後的兩個領域,任務改變了。

樞紐之前,我們收集了三種形狀,卻還說不清楚它們的差別。瀏覽器 API 讓「呼叫端還需要知道什麼?」成為可以繼續追的問句。互動行為與真實 DOM,則帶著這個問句走到新的領域,試它能走多遠。

如果只沿著抽象這條線回看,這三十天像是兩幕。前一幕看見差異,還不知道怎麼往下問;後一幕拿著樞紐找到的問法,到更遠的地方再試一次。

遠端世界一開始問的是:這份資料是誰的?走到真實 DOM,我們開始問:這個判斷是誰的?

不過,到目前為止,我們看的都是別人留下的邊界。輪到自己設計時,哪些判斷該由模組承擔,哪些又該交還給呼叫端?


上一篇
【 Day 27 】六個問題問完了,重複出現的是哪幾個詞?
系列文
再造輪子:30 天臨摹 React Hook 函式庫,探索背後的設計哲學 共 29 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言